/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *	This file is part of the Tiny Look and Feel                                *
 *  Copyright 2003 - 2008  Hans Bickel                                         *
 *                                                                             *
 *  For licensing information and credits, please refer to the                 *
 *  comment in file de.muntjak.tinylookandfeel.TinyLookAndFeel                 *
 *                                                                             *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package de.muntjak.tinylookandfeel;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.List;
import java.util.Vector;

import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.event.ListDataEvent;
import javax.swing.filechooser.FileSystemView;
import javax.swing.plaf.basic.BasicDirectoryModel;
import javax.swing.plaf.basic.BasicFileChooserUI;

import sun.awt.shell.ShellFolder;

/**
 * A copy of BasicDirectoryModel with access to the fileCache.
 * @author Hans Bickel
 *
 */
public class TinyDirectoryModel extends BasicDirectoryModel {

	private JFileChooser filechooser = null;

	// PENDING(jeff) pick the size more sensibly
	private Vector fileCache = new Vector(50);

	private LoadFilesThread loadThread = null;

	private Vector files = null;

	private Vector directories = null;

	private int fetchID = 0;

	private PropertyChangeSupport changeSupport;

	private boolean busy = false;

	public TinyDirectoryModel(JFileChooser fc) {
		super(fc);
		
		this.filechooser = fc;
		validateFileCache();
	}

	public void propertyChange(PropertyChangeEvent e) {
		String prop = e.getPropertyName();
		if(prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY
			|| prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY
			|| prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY
			|| prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY
			|| prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
			validateFileCache();
		}
		else if("UI".equals(prop)) {
			Object old = e.getOldValue();
			if(old instanceof BasicFileChooserUI) {
				BasicFileChooserUI ui = (BasicFileChooserUI)old;
				BasicDirectoryModel model = ui.getModel();
				if(model != null) {
					model.invalidateFileCache();
				}
			}
		}
		else if("JFileChooserDialogIsClosingProperty".equals(prop)) {
			invalidateFileCache();
		}
	}

	/**
	 * This method is used to interrupt file loading thread.
	 */
	public void invalidateFileCache() {
		if(loadThread != null) {
			loadThread.interrupt();
			loadThread.cancelRunnables();
			loadThread = null;
		}
	}

	public Vector getDirectories() {
		synchronized (fileCache) {
			if(directories != null) {
				return directories;
			}
			Vector fls = getFiles();
			return directories;
		}
	}

	public Vector getFiles() {
		synchronized (fileCache) {
			if(files != null) {
				return files;
			}
			files = new Vector();
			directories = new Vector();
			directories.addElement(filechooser.getFileSystemView()
				.createFileObject(filechooser.getCurrentDirectory(), ".."));

			for(int i = 0; i < getSize(); i++) {
				File f = (File)fileCache.get(i);
				if(filechooser.isTraversable(f)) {
					directories.add(f);
				}
				else {
					files.add(f);
				}
			}
			return files;
		}
	}

	public void validateFileCache() {
		// Note: The super constructor will call this method
		// but at this time filechooser is null
		if(filechooser == null) return;
		
		File currentDirectory = filechooser.getCurrentDirectory();
		if(currentDirectory == null) {
			return;
		}
		if(loadThread != null) {
			loadThread.interrupt();
			loadThread.cancelRunnables();
		}

		setBusy(true, ++fetchID);

		loadThread = new LoadFilesThread(currentDirectory, fetchID);
		loadThread.start();
	}

	/**
	 * Renames a file in the underlying file system.
	 *
	 * @param oldFile a <code>File</code> object representing
	 *        the existing file
	 * @param newFile a <code>File</code> object representing
	 *        the desired new file name
	 * @return <code>true</code> if rename succeeded,
	 *        otherwise <code>false</code>
	 * @since 1.4
	 */
	public boolean renameFile(File oldFile, File newFile) {
		synchronized (fileCache) {
			if(oldFile.renameTo(newFile)) {
				validateFileCache();
				return true;
			}
			return false;
		}
	}

	public void fireContentsChanged() {
		// System.out.println("TinyDirectoryModel: firecontentschanged");
		fireContentsChanged(this, 0, getSize() - 1);
	}

	public int getSize() {
		return fileCache.size();
	}

	public boolean contains(Object o) {
		return fileCache.contains(o);
	}

	public int indexOf(Object o) {
		return fileCache.indexOf(o);
	}

	public Object getElementAt(int index) {
		return fileCache.get(index);
	}
	
	public Vector getFileCache() {
		return fileCache;
	}

	/**
	 * Obsolete - not used.
	 */
	public void intervalAdded(ListDataEvent e) {
	}

	/**
	 * Obsolete - not used.
	 */
	public void intervalRemoved(ListDataEvent e) {
	}

	protected void sort(Vector v) {
		ShellFolder.sortFiles(v);
	}

	// Obsolete - not used
	protected boolean lt(File a, File b) {
		// First ignore case when comparing
		int diff = a.getName().toLowerCase().compareTo(
			b.getName().toLowerCase());
		if(diff != 0) {
			return diff < 0;
		}
		else {
			// May differ in case (e.g. "mail" vs. "Mail")
			return a.getName().compareTo(b.getName()) < 0;
		}
	}

	class LoadFilesThread extends Thread {

		File currentDirectory = null;

		int fid;

		Vector runnables = new Vector(10);

		public LoadFilesThread(File currentDirectory, int fid) {
			super("Basic L&F File Loading Thread");
			this.currentDirectory = currentDirectory;
			this.fid = fid;
		}

		private void invokeLater(Runnable runnable) {
			runnables.addElement(runnable);
			SwingUtilities.invokeLater(runnable);
		}

		public void run() {
			run0();
			setBusy(false, fid);
		}

		public void run0() {
			FileSystemView fileSystem = filechooser.getFileSystemView();

			File[] list = fileSystem.getFiles(currentDirectory, filechooser
				.isFileHidingEnabled());

			Vector acceptsList = new Vector();

			if(isInterrupted()) {
				return;
			}

			// run through the file list, add directories and selectable files to fileCache
			for(int i = 0; i < list.length; i++) {
				if(filechooser.accept(list[i])) {
					acceptsList.addElement(list[i]);
				}
			}

			if(isInterrupted()) {
				return;
			}

			// First sort alphabetically by filename
			sort(acceptsList);

			Vector newDirectories = new Vector(50);
			Vector newFiles = new Vector();
			// run through list grabbing directories in chunks of ten
			for(int i = 0; i < acceptsList.size(); i++) {
				File f = (File)acceptsList.elementAt(i);
				boolean isTraversable = filechooser.isTraversable(f);
				if(isTraversable) {
					newDirectories.addElement(f);
				}
				else if(!isTraversable && filechooser.isFileSelectionEnabled()) {
					newFiles.addElement(f);
				}
				if(isInterrupted()) {
					return;
				}
			}

			Vector newFileCache = new Vector(newDirectories);
			newFileCache.addAll(newFiles);

			int newSize = newFileCache.size();
			int oldSize = fileCache.size();

			if(newSize > oldSize) {
				//see if interval is added
				int start = oldSize;
				int end = newSize;
				for(int i = 0; i < oldSize; i++) {
					if(!newFileCache.get(i).equals(fileCache.get(i))) {
						start = i;
						for(int j = i; j < newSize; j++) {
							if(newFileCache.get(j).equals(fileCache.get(i))) {
								end = j;
								break;
							}
						}
						break;
					}
				}
				if(start >= 0
					&& end > start
					&& newFileCache.subList(end, newSize).equals(
						fileCache.subList(start, oldSize))) {
					if(isInterrupted()) {
						return;
					}
					invokeLater(new DoChangeContents(newFileCache.subList(
						start, end), start, null, 0, fid));
					newFileCache = null;
				}
			}
			else if(newSize < oldSize) {
				//see if interval is removed
				int start = -1;
				int end = -1;
				for(int i = 0; i < newSize; i++) {
					if(!newFileCache.get(i).equals(fileCache.get(i))) {
						start = i;
						end = i + oldSize - newSize;
						break;
					}
				}
				if(start >= 0
					&& end > start
					&& fileCache.subList(end, oldSize).equals(
						newFileCache.subList(start, newSize))) {
					if(isInterrupted()) {
						return;
					}
					invokeLater(new DoChangeContents(null, 0, new Vector(
						fileCache.subList(start, end)), start, fid));
					newFileCache = null;
				}
			}
			if(newFileCache != null && !fileCache.equals(newFileCache)) {
				if(isInterrupted()) {
					cancelRunnables(runnables);
				}
				invokeLater(new DoChangeContents(newFileCache, 0, fileCache, 0,
					fid));
			}
		}

		public void cancelRunnables(Vector runnables) {
			for(int i = 0; i < runnables.size(); i++) {
				((DoChangeContents)runnables.elementAt(i)).cancel();
			}
		}

		public void cancelRunnables() {
			cancelRunnables(runnables);
		}
	}

	/**
	 * Adds a PropertyChangeListener to the listener list. The listener is
	 * registered for all bound properties of this class.
	 * <p>
	 * If <code>listener</code> is <code>null</code>,
	 * no exception is thrown and no action is performed.
	 *
	 * @param    listener  the property change listener to be added
	 *
	 * @see #removePropertyChangeListener
	 * @see #getPropertyChangeListeners
	 *
	 * @since 1.6
	 */
	public void addPropertyChangeListener(PropertyChangeListener listener) {
		if(changeSupport == null) {
			changeSupport = new PropertyChangeSupport(this);
		}
		changeSupport.addPropertyChangeListener(listener);
	}

	/**
	 * Removes a PropertyChangeListener from the listener list.
	 * <p>
	 * If listener is null, no exception is thrown and no action is performed.
	 *
	 * @param listener the PropertyChangeListener to be removed
	 *
	 * @see #addPropertyChangeListener
	 * @see #getPropertyChangeListeners
	 *
	 * @since 1.6
	 */
	public void removePropertyChangeListener(PropertyChangeListener listener) {
		if(changeSupport != null) {
			changeSupport.removePropertyChangeListener(listener);
		}
	}

	/**
	 * Returns an array of all the property change listeners
	 * registered on this component.
	 *
	 * @return all of this component's <code>PropertyChangeListener</code>s
	 *         or an empty array if no property change
	 *         listeners are currently registered
	 *
	 * @see      #addPropertyChangeListener
	 * @see      #removePropertyChangeListener
	 * @see      java.beans.PropertyChangeSupport#getPropertyChangeListeners
	 *
	 * @since 1.6
	 */
	public PropertyChangeListener[] getPropertyChangeListeners() {
		if(changeSupport == null) {
			return new PropertyChangeListener[0];
		}
		return changeSupport.getPropertyChangeListeners();
	}

	/**
	 * Support for reporting bound property changes for boolean properties. 
	 * This method can be called when a bound property has changed and it will
	 * send the appropriate PropertyChangeEvent to any registered
	 * PropertyChangeListeners.
	 *
	 * @param propertyName the property whose value has changed
	 * @param oldValue the property's previous value
	 * @param newValue the property's new value
	 *
	 * @since 1.6
	 */
	protected void firePropertyChange(String propertyName, boolean oldValue,
		boolean newValue) {
		if(changeSupport != null) {
			changeSupport.firePropertyChange(propertyName, oldValue, newValue);
		}
	}

	/**
	 * Set the busy state for the model. The model is considered
	 * busy when it is running a separate (interruptable)
	 * thread in order to load the contents of a directory.
	 */
	private synchronized void setBusy(final boolean busy, int fid) {
		if(fid == fetchID) {
			boolean oldValue = this.busy;
			this.busy = busy;

			if(changeSupport != null && busy != oldValue) {
				SwingUtilities.invokeLater(new Runnable() {

					public void run() {
						firePropertyChange("busy", !busy, busy);
					}
				});
			}
		}
	}

	class DoChangeContents implements Runnable {

		private List addFiles;

		private List remFiles;

		private boolean doFire = true;

		private int fid;

		private int addStart = 0;

		private int remStart = 0;

		private int change;

		public DoChangeContents(List addFiles, int addStart, List remFiles,
			int remStart, int fid) {
			this.addFiles = addFiles;
			this.addStart = addStart;
			this.remFiles = remFiles;
			this.remStart = remStart;
			this.fid = fid;
		}

		synchronized void cancel() {
			doFire = false;
		}

		public synchronized void run() {
			if(fetchID == fid && doFire) {
				int remSize = (remFiles == null) ? 0 : remFiles.size();
				int addSize = (addFiles == null) ? 0 : addFiles.size();
				synchronized (fileCache) {
					if(remSize > 0) {
						fileCache.removeAll(remFiles);
					}
					if(addSize > 0) {
						fileCache.addAll(addStart, addFiles);
					}
					files = null;
					directories = null;
				}
				if(remSize > 0 && addSize == 0) {
					fireIntervalRemoved(TinyDirectoryModel.this, remStart,
						remStart + remSize - 1);
				}
				else if(addSize > 0 && remSize == 0
					&& fileCache.size() > addSize) {
					fireIntervalAdded(TinyDirectoryModel.this, addStart,
						addStart + addSize - 1);
				}
				else {
					fireContentsChanged();
				}
			}
		}
	}
}
